//! DMA (Direct Memory Access) management in Zig
//! SPDX-License-Identifier: MPL-2.0

const std = @import("std");
const lib = @import("lib.zig");
const frame = @import("frame.zig");

const Paddr = lib.Paddr;
const Daddr = lib.Daddr;
const MemoryError = lib.MemoryError;

// DMA direction
pub const DmaDirection = enum {
    ToDevice,
    FromDevice,
    Bidirectional,
};

// DMA coherent memory
pub const DmaCoherent = struct {
    vaddr: usize,
    paddr: Paddr,
    size: usize,
    frame_ref: frame.Frame,

    const Self = @This();

    pub fn init(size: usize, frame_allocator: frame.FrameAllocator, allocator: std.mem.Allocator) !Self {
        const options = frame.FrameAllocOptions.new().withZeroed(true);
        const dma_frame = try frame_allocator.allocFrame(options, allocator);
        
        return Self{
            .vaddr = lib.paddrToVaddr(dma_frame.startPaddr()),
            .paddr = dma_frame.startPaddr(),
            .size = size,
            .frame_ref = dma_frame,
        };
    }

    pub fn deinit(self: Self, frame_allocator: frame.FrameAllocator) void {
        frame_allocator.deallocFrame(self.frame_ref);
    }

    pub fn daddr(self: Self) Daddr {
        return @intCast(self.paddr);
    }

    pub fn asSlice(self: Self) []u8 {
        return @as([*]u8, @ptrFromInt(self.vaddr))[0..self.size];
    }

    pub fn asMutSlice(self: Self) []u8 {
        return @as([*]u8, @ptrFromInt(self.vaddr))[0..self.size];
    }
};

// DMA stream for streaming DMA operations
pub const DmaStream = struct {
    segments: std.ArrayList(DmaSegment),
    direction: DmaDirection,
    allocator: std.mem.Allocator,

    const DmaSegment = struct {
        paddr: Paddr,
        size: usize,
    };

    const Self = @This();

    pub fn init(direction: DmaDirection, allocator: std.mem.Allocator) Self {
        return Self{
            .segments = std.ArrayList(DmaSegment).init(allocator),
            .direction = direction,
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *Self) void {
        self.segments.deinit();
    }

    pub fn addSegment(self: *Self, paddr: Paddr, size: usize) !void {
        try self.segments.append(DmaSegment{
            .paddr = paddr,
            .size = size,
        });
    }

    pub fn totalSize(self: Self) usize {
        var total: usize = 0;
        for (self.segments.items) |segment| {
            total += segment.size;
        }
        return total;
    }

    pub fn segmentCount(self: Self) usize {
        return self.segments.items.len;
    }

    pub fn getSegment(self: Self, index: usize) ?DmaSegment {
        if (index >= self.segments.items.len) return null;
        return self.segments.items[index];
    }
};

// HasDaddr trait for types that have DMA addresses
pub fn HasDaddr(comptime T: type) type {
    return struct {
        pub fn hasDaddr(self: T) Daddr {
            return switch (@typeInfo(T)) {
                .Struct => {
                    if (@hasDecl(T, "daddr")) {
                        return self.daddr();
                    } else if (@hasDecl(T, "paddr")) {
                        return @intCast(self.paddr());
                    } else {
                        @compileError("Type must have either daddr() or paddr() method");
                    }
                },
                else => @compileError("HasDaddr can only be applied to struct types"),
            };
        }
    };
}

// Tests
test "DMA coherent memory" {
    const testing = std.testing;
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Mock frame allocator for testing
    var buddy = frame.BuddyAllocator.init(0x100000, 0x100000);
    const frame_allocator = buddy.frameAllocator();

    var dma_mem = try DmaCoherent.init(4096, frame_allocator, allocator);
    defer dma_mem.deinit(frame_allocator);

    try testing.expectEqual(@as(usize, 4096), dma_mem.size);
    try testing.expect(dma_mem.paddr != 0);
    try testing.expect(dma_mem.daddr() != 0);
}

test "DMA stream" {
    const testing = std.testing;
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var stream = DmaStream.init(DmaDirection.ToDevice, allocator);
    defer stream.deinit();

    try stream.addSegment(0x1000, 4096);
    try stream.addSegment(0x2000, 8192);

    try testing.expectEqual(@as(usize, 2), stream.segmentCount());
    try testing.expectEqual(@as(usize, 12288), stream.totalSize());

    const first_segment = stream.getSegment(0).?;
    try testing.expectEqual(@as(Paddr, 0x1000), first_segment.paddr);
    try testing.expectEqual(@as(usize, 4096), first_segment.size);
}